/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.jfonia.view.elements;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.swing.InputMap;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.jfonia.connect5.basics.BasicValueNode;
import org.jfonia.connect5.basics.MutableValueNode;
import org.jfonia.connect5.basics.ValueNode;
import org.jfonia.connect5.basics.Observer;
import org.jfonia.connect5.relations.Equal;
import org.jfonia.connect5.relations.UnidirectionalEqual;
import org.jfonia.constants.ViewConstants;
import org.jfonia.model.elements.MusicalElement;
import org.jfonia.view.SequenceView;
import org.jfonia.view.main.JTextField;
import org.jfonia.notation.Notation;

/**
 *
 * @author Rik Bauwens
 */
public abstract class AbstractField extends JTextField implements IElement, 
        DocumentListener, MouseListener, KeyListener
{
        //Observers
        private Set<Observer> observers = new LinkedHashSet<Observer>();

        protected SequenceView sequenceView;

        private MutableValueNode<Double> xNode;
        private MutableValueNode<Double> yNode;
        private MutableValueNode<Double> widthNode;
        private MutableValueNode<Double> heightNode;

        private MutableValueNode<Double> preferredWidthNode;

        private MutableValueNode<Double> frameWidth;
        private ValueNode<Double> staffHeight;

        private UnidirectionalEqual frameXRelation;
        private UnidirectionalEqual frameWidthRelation;

        protected MusicalElement musicalElement;

        private MutableValueNode<Integer> beginNode;
        private MutableValueNode<Integer> endNode;
        private Equal<Integer> beginMusicalTimeRelation;
        private Equal<Integer> endMusicalTimeRelation;

        private boolean empty;
        private boolean selected;
        private boolean highlighted;

        public AbstractField(ValueNode<Double> frameX, ValueNode<Double> staffY,
                             ValueNode<Double> frameWidth, MutableValueNode<Double> staffHeight,
                             MusicalElement musicalElement, SequenceView sequenceView)
        {
                super();
                this.sequenceView = sequenceView;

                this.frameWidth = new BasicValueNode<Double>(0.0);
                setFrameWidthNode(frameWidth);
                this.staffHeight = staffHeight;

                empty = true;
                selected = false;
                highlighted = false;

                this.xNode = new BasicValueNode<Double>(0.0);
                //Later an offset can be added here
                setFrameXNode(frameX);
                this.yNode = new BasicValueNode<Double>(0.0);
                //Later an offset can be added here
                new UnidirectionalEqual(staffY, yNode);
                this.widthNode = new BasicValueNode<Double>(0.0);
                this.preferredWidthNode = new BasicValueNode<Double>(0.0);
                //MaxRelation to make sure that the width of the frame
                //is the largest width of its elements
                //new Equal<Double>(this.widthNode, preferredWidthNode);
                //new DoubleMaxRelation(frameWidth, this.widthNode);
                this.heightNode = new BasicValueNode<Double>(0.0);
                //MaxRelation to make sure that the height of the staff
                //is the largest height of its elements
                //new DoubleMaxRelation(staffHeight, heightNode);
                new Equal<Double>(staffHeight, heightNode);

                beginNode = new BasicValueNode<Integer>(0);
                beginNode.addObserver(new Observer()
                {
                        public void onNotify(Object source)
                        {
                                notifyObservers();
                        }
                });
                endNode = new BasicValueNode<Integer>(0);
                endNode.addObserver(new Observer()
                {
                        public void onNotify(Object source)
                        {
                                notifyObservers();
                        }
                });
                setMusicalElement(musicalElement);

                //Add observers (x, y, width, height) and set options
                init();
                handleTextChanged();
        }

        private void init()
        {
                //Set bounds of this textfield
                //As width and height, the width/height of the frame/staff is used
                setBounds(xNode.getValue().intValue(), yNode.getValue().intValue(),
                          frameWidth.getValue().intValue(), staffHeight.getValue().intValue());
                setBackground(ViewConstants.ELEMENT_COLOR);
                //setEnabled(false);
                setEditable(false);
                setBorder(null);
                //Remove all of the default key bindings
                //getInputMap().setParent(null);
                //Remove key bindings for move left/right, up/down
                InputMap inputMap = getInputMap();
                inputMap.put(KeyStroke.getKeyStroke(KeyEvent.getKeyText(ViewConstants.MOVE_LEFT_KEYCODE).toUpperCase()), "");
                inputMap.put(KeyStroke.getKeyStroke(KeyEvent.getKeyText(ViewConstants.MOVE_RIGHT_KEYCODE).toUpperCase()), "");
                inputMap.put(KeyStroke.getKeyStroke(KeyEvent.getKeyText(ViewConstants.MOVE_UP_KEYCODE).toUpperCase()), "");
                inputMap.put(KeyStroke.getKeyStroke(KeyEvent.getKeyText(ViewConstants.MOVE_DOWN_KEYCODE).toUpperCase()), "");
                addMouseListener(this);
                addKeyListener(this);
                getDocument().addDocumentListener(this);
                xNode.addObserver( new Observer() {
                        public void onNotify(Object source)
                        {
                                setLocation(xNode.getValue().intValue(), getY());
                        }
		});
		yNode.addObserver( new Observer() {
			public void onNotify(Object source)
			{
				setLocation(getX(), yNode.getValue().intValue());
			}
		});
                frameWidth.addObserver( new Observer() {
			public void onNotify(Object source)
			{
                                setBounds(getX(), getY(),
                                        frameWidth.getValue().intValue(), getHeight());
			}
		});
                staffHeight.addObserver( new Observer() {
			public void onNotify(Object source)
			{
				setBounds(getX(), getY(),
                                        getWidth(), staffHeight.getValue().intValue());
			}
		});
        }

        public int getTickPosition()
        {
                return -1;
        }

        private void setMusicalElement(MusicalElement musicalElement)
        {
                if(this.musicalElement != null)
                {
                        beginMusicalTimeRelation.switchOff();
                        endMusicalTimeRelation.switchOff();
                }
                beginMusicalTimeRelation = new Equal(musicalElement.getBeginNode(), beginNode);
                endMusicalTimeRelation = new Equal(musicalElement.getEndNode(), endNode);
                this.musicalElement = musicalElement;
                empty = false;
        }

        public void removeMusicalElement()
        {
                if(this.musicalElement != null)
                {
                        beginMusicalTimeRelation.switchOff();
                        endMusicalTimeRelation.switchOff();
                }
                beginMusicalTimeRelation = null;
                endMusicalTimeRelation = null;
                this.musicalElement = null;
                empty = true;
        }

        public boolean contains(MusicalElement musicalElement)
        {
                return this.musicalElement == musicalElement;
        }

        public boolean isEmpty()
        {
                return empty;
        }

        //Method that changes the value of the widthNode/heightNode when the text
        //in the textfield is changed
        private void handleTextChanged()
        {
                preferredWidthNode.setValue(new Double(getFontMetrics(getFont()).stringWidth(getText())));
                heightNode.setValue(new Double(getFontMetrics(getFont()).getHeight()));
        }

        private void setHighlighted(boolean highlighted)
        {
                
                if(!selected)
                {
                        if(highlighted)
                                setBackground(ViewConstants.ELEMENT_HIGHLIGHTED_COLOR);
                        else
                                setBackground(ViewConstants.ELEMENT_COLOR);
                        this.highlighted = highlighted;
                }
                else
                {
                        this.highlighted = false;
                }
        }

        public void setSelected(boolean selected)
        {
                if(selected)
                        setBackground(ViewConstants.ELEMENT_SELECTED_COLOR);
                else
                        setBackground(ViewConstants.ELEMENT_COLOR);
                setEditable(selected);
                if(selected)
                        requestFocus();
                this.selected = selected;
        }

        public MutableValueNode<Double> getXNode()
        {
                return xNode;
        }

        public MutableValueNode<Double> getYNode()
        {
                return yNode;
        }

        public MutableValueNode<Double> getWidthNode()
        {
                return widthNode;
        }

        public MutableValueNode<Double> getHeightNode()
        {
                return heightNode;
        }

        public int getBegin()
        {
                return getBeginNode().getValue();
        }

        public int getEnd()
        {
                return getEndNode().getValue();
        }

        public final void setFrameXNode(ValueNode<Double> frameXNode)
        {
                if(frameXRelation != null)
                        frameXRelation.switchOff();
                frameXRelation = new UnidirectionalEqual(frameXNode, xNode);
        }

        public final void setFrameWidthNode(ValueNode<Double> frameWidthNode)
        {
                if(frameWidthRelation != null)
                        frameWidthRelation.switchOff();
                frameWidthRelation = new UnidirectionalEqual(frameWidthNode, this.frameWidth);
        }

        public ValueNode<Double> getPreferredWidthNode()
        {
                return preferredWidthNode;
        }

        public ValueNode<Integer> getBeginNode()
        {
                return beginNode;
        }

        public ValueNode<Integer> getEndNode()
        {
                return endNode;
        }

        //Override setLocation(x, y)
        //When x/y are changed, the nodes need to be updated as well
        @Override
        public void setLocation(int x, int y)
        {
                super.setLocation(x, y);
                //Avoid propagation
                if(x != getX())
                        xNode.setValue(new Double(x));
                if(y != getY())
                        yNode.setValue(new Double(y));
        }

        //Call setLocation(x, y)
        @Override
        public void setLocation(Point p)
        {
                setLocation(p.x, p.y);
        }

        //Override the setBounds(x, y, width, height) method
        //When new boundaries are set, the nodes are aught to be updated
        @Override
        public void setBounds(int x, int y, int width, int height)
        {
                super.setBounds(x, y, width, height);
                //Avoid propagation
                if(x != getX())
                        xNode.setValue(new Double(x));
                if(y != getY())
                        yNode.setValue(new Double(y));
                if(width != getWidth())
                        widthNode.setValue(new Double(width));
                if(height != getHeight())
                        heightNode.setValue(new Double(height));
        }

        //Call setBounds (x, y, width, height)
        @Override
        public void setBounds(Rectangle r)
        {
                setBounds(r.x, r.y, r.width, r.height);
        }

        //When text is inserted in the textfield, the widthNode/heightNode
        //must be updated
        public void insertUpdate(DocumentEvent e)
        {
                handleTextChanged();
        }

        //When text is removed from the textfield, the widthNode/heightNode
        //must be updated
        public void removeUpdate(DocumentEvent e)
        {
                handleTextChanged();
        }

        //When text is changed from the textfield, the widthNode/heightNode
        //must be updated
        public void changedUpdate(DocumentEvent e)
        {
                handleTextChanged();
        }

        public void selectPreviousPart()
        {
                IElement element = sequenceView.previousElement(this);
                if(element != null)
                        Notation.getInstance().setSelectedElement(element);
        }

        public void selectNextPart()
        {
                IElement element = sequenceView.nextElement(this);
                if(element != null)
                        Notation.getInstance().setSelectedElement(element);
        }

        public void selectPartAbove()
        {
                IElement element = sequenceView.elementAbove(this);
                if(element != null)
                        Notation.getInstance().setSelectedElement(element);
        }

        public void selectPartBelow()
        {
                IElement element = sequenceView.elementBelow(this);
                if(element != null)
                        Notation.getInstance().setSelectedElement(element);
        }

        public void caretPrevious(boolean keepSelected)
        {
                shiftCaret(-1, keepSelected);
        }

        public void caretNext(boolean keepSelected)
        {
                shiftCaret(1, keepSelected);
        }

        public void shiftCaret(int posCount, boolean keepSelected)
        {
                int caretPosition = getCaretPosition();
                int textLength = getText().length();
                //If this method is called by a previous or next abstractfield,
                //this abstractfield has no caret yet, so set it to the end
                //of the frame
                if(!hasFocus())
                        caretPosition = textLength;

                //Move caret to a frame on the left
                if(caretPosition + posCount < 0)
                {
                        IElement element = sequenceView.previousElement(this);
                        if(element != null)
                                element.shiftCaret(posCount + caretPosition + 1, 
                                        keepSelected);
                }
                //Move caret to a frame on the right
                else if(caretPosition + posCount > textLength)
                {
                        IElement element = sequenceView.nextElement(this);
                        if(element != null)
                                element.shiftCaret(posCount - (textLength - caretPosition) - 1, 
                                        keepSelected);
                }
                else
                {
                        requestFocus();
                        setEditable(true);
                        setCaretPosition(caretPosition + posCount);
                }
        }

        //Handle MouseEvent
        public void mouseClicked(MouseEvent e)
        {
                Notation.getInstance().setSelectedElement(this);
        }

        public void mousePressed(MouseEvent e) {}

        public void mouseReleased(MouseEvent e) {}

        public void mouseEntered(MouseEvent e)
        {
                setHighlighted(true);
        }

        public void mouseExited(MouseEvent e) {
                setHighlighted(false);
        }

        public void keyReleased(KeyEvent e)
        {
                if(e.isControlDown())
                        return;
                switch(e.getKeyCode())
                {
                        case ViewConstants.TEXT_ENTER_KEYCODE : setEditable(!isEditable());setCaretPosition(getText().length());//setEnabled(!isEnabled());
                        break;
                        case ViewConstants.MOVE_LEFT_KEYCODE : if(isEditable()) caretPrevious(e.isControlDown()); else selectPreviousPart();
                        break;
                        case ViewConstants.MOVE_RIGHT_KEYCODE : if(isEditable()) caretNext(e.isControlDown()); else selectNextPart();
                        break;
                        case ViewConstants.MOVE_UP_KEYCODE : selectPartAbove();
                        break;
                        case ViewConstants.MOVE_DOWN_KEYCODE : selectPartBelow();
                        break;
                        //default: System.out.println("none");
                }
        }

        public void keyTyped(KeyEvent e) {}

        public void keyPressed(KeyEvent e) {}

	public void addObserver(Observer o)
	{
		if (!observers.contains(o))
			observers.add(o);
	}

	public boolean containsObserver(Observer o)
	{
		return observers.contains(o);
	}

	public void removeObserver(Observer o)
	{
		observers.remove(o);
	}

	public void notifyObservers()
	{
		for (Observer o: observers)
			o.onNotify(this);
	}

	public void notifyObserversExcept(Observer except)
	{
		for (Observer o: observers)
			if (o != except)
				o.onNotify(this);
	}
}
